package com.apes.framework.util;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import lombok.extern.slf4j.Slf4j;
import org.apache.log4j.Logger;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.util.StringUtils;

/**
 * 功能：类扫描工具类
 *
 * @author xul
 * @create 2018-04-11 14:27
 */
@Slf4j
public class ClassPathScanUtil {

    private static final Logger logger = Logger.getLogger(ClassPathScanUtil.class);

    private boolean excludeInner = true;

    private boolean checkInOrEx = true;

    private List<String> classFilters = null;

    public ClassPathScanUtil() {
    }

    public ClassPathScanUtil(Boolean excludeInner, Boolean checkInOrEx, List<String> classFilters) {
        this.excludeInner = excludeInner;
        this.checkInOrEx = checkInOrEx;
        this.classFilters = classFilters;
    }

    public Set getPackageAllClasses(String basePackage, boolean recursive) {
        Set classes = new LinkedHashSet();
        String packageName = basePackage;
        if (packageName.endsWith(".")) {
            packageName = packageName.substring(0, packageName.lastIndexOf('.'));
        }
        String package2Path = packageName.replace('.', '/');
        Enumeration<URL> dirs;
        try {
            dirs = Thread.currentThread().getContextClassLoader().getResources(package2Path);
            while (dirs.hasMoreElements()) {
                URL url = dirs.nextElement();
                String protocol = url.getProtocol();
                if ("file".equals(protocol)) {
                    logger.info("扫描file类型的class文件....");
                    String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
                    doScanPackageClassesByFile(classes, packageName, filePath, recursive);
                } else if ("jar".equals(protocol)) {
                    logger.info("扫描jar文件中的类....");
                    doScanPackageClassesByJar(packageName, url, recursive, classes);
                }
            }
        } catch (IOException e) {
            logger.error("IOException error:", e);
        }
        return classes;
    }

    private void doScanPackageClassesByJar(String basePackage, URL url, final boolean recursive, Set classes) {
        String packageName = basePackage;
        String package2Path = packageName.replace('.', '/');
        JarFile jar;
        try {
            jar = ((JarURLConnection) url.openConnection()).getJarFile();
            Enumeration<JarEntry> entries = jar.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                String name = entry.getName();
                if (!name.startsWith(package2Path) || entry.isDirectory()) {
                    continue;
                }
                // 判断是否递归搜索子包
                if (!recursive && name.lastIndexOf('/') != package2Path.length()) {
                    continue;
                }
                // 判断是否过滤 inner class
                if (this.excludeInner && name.indexOf('$') != -1) {
                    logger.info("exclude inner class with name:" + name);
                    continue;
                }
                String classSimpleName = name.substring(name.lastIndexOf('/') + 1);
                // 判定是否符合过滤条件
                if (this.filterClassName(classSimpleName)) {
                    String className = name.replace('/', '.');
                    className = className.substring(0, className.length() - 6);
                    try {
                        classes.add(Thread.currentThread().getContextClassLoader().loadClass(className));
                    } catch (ClassNotFoundException e) {
                        logger.error("Class.forName error:", e);
                    }
                }
            }
        } catch (IOException e) {
            logger.error("IOException error:", e);
        }
    }

    private void doScanPackageClassesByFile(Set classes, String packageName, String packagePath, boolean recursive) {
        File dir = new File(packagePath);
        if (!dir.exists() || !dir.isDirectory()) {
            return;
        }
        final boolean fileRecursive = recursive;
        File[] dirFiles = dir.listFiles(new FileFilter() {
            // 自定义文件过滤规则
            public boolean accept(File file) {
                if (file.isDirectory()) {
                    return fileRecursive;
                }
                String filename = file.getName();
                if (excludeInner && filename.indexOf('$') != -1) {
                    logger.info("exclude inner class with name:" + filename);
                    return false;
                }
                return filterClassName(filename);
            }
        });
        for (File file : dirFiles) {
            if (file.isDirectory()) {
                doScanPackageClassesByFile(classes, packageName + "." + file.getName(), file.getAbsolutePath(), recursive);
            } else {
                String className = file.getName().substring(0, file.getName().length() - 6);
                try {
                    classes.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className));
                } catch (ClassNotFoundException e) {
                    logger.error("IOException error:", e);
                }
            }
        }
    }

    private boolean filterClassName(String className) {
        if (!className.endsWith(".class")) {
            return false;
        }
        if (null == this.classFilters || this.classFilters.isEmpty()) {
            return true;
        }
        String tmpName = className.substring(0, className.length() - 6);
        boolean flag = false;
        for (String str : classFilters) {
            String regex = "^" + str.replace("*", ".*") + "$";
            Pattern p = Pattern.compile(regex);
            if (p.matcher(tmpName).find()) {
                flag = true;
                break;
            }
        }
        return (checkInOrEx && flag) || (!checkInOrEx && !flag);
    }

    public boolean isExcludeInner() {
        return excludeInner;
    }

    public boolean isCheckInOrEx() {
        return checkInOrEx;
    }

    public List getClassFilters() {
        return classFilters;
    }

    public void setExcludeInner(boolean pExcludeInner) {
        excludeInner = pExcludeInner;
    }

    public void setCheckInOrEx(boolean pCheckInOrEx) {
        checkInOrEx = pCheckInOrEx;
    }

    public void setClassFilters(List pClassFilters) {
        classFilters = pClassFilters;
    }

//    public static void main(String[] args) {
//        List<String> classFilters = new ArrayList<>();
//        classFilters.add("Cooperation");
//        ClassPathScanUtil handler = new ClassPathScanUtil(false, true, classFilters);
//        Set classSet = handler.getPackageAllClasses("com.apes.scm", true);
//        for (Iterator iterator = classSet.iterator(); iterator.hasNext(); ) {
//            Class clazz = (Class) iterator.next();
//            System.out.println(clazz.getName());
//        }
//    }

    /**
     * 功能：获取外部配置文件目录
     *
     * @return
     */
    public static String getConfigFilePath() {
        String configFilePath = "/config/";
        String rootPath = new File("").getAbsolutePath();
        File file = new File(rootPath, configFilePath);
        if (file.exists()) {
            return file.getAbsolutePath();
        }

        configFilePath = "../config/";
        file = new File(rootPath, configFilePath);
        if (file.exists()) {
            return file.getAbsolutePath();
        }
        return null;
    }

    /**
     * 获取当前上下方ClassPath
     **/
    private String buildClassPath() {
        URLClassLoader parentClassLoader = (URLClassLoader) this.getClass().getClassLoader();
        StringBuilder sb = new StringBuilder();
        for (URL url : parentClassLoader.getURLs()) {
            String p = url.getFile();
            sb.append(p).append(File.pathSeparator); //路径分割符linux为:window系统为;
        }
        return sb.toString();
    }

    private static final String PROPERTIES_FILE_NAMES = "classpath.properties";
    private static final String CLASS_PATH_VALUE = "classpath";
    private static final String BOOT_INF = "BOOT-INF/";
    private static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
    private static final String BOOT_INF_LIB = "BOOT-INF/lib/";

    public static String getClassPath() {
        URLConnection connection = getURLConnection();
        if (!isJarFile(connection)) return null;
        String classPathDir = getClassPathDir();
        Properties properties = getProperties(classPathDir);
        String classPathValue = properties.getProperty(CLASS_PATH_VALUE);
        if (StringUtils.isEmpty(classPathValue)) {
            try {
                JarFile jarFile = ((JarURLConnection) connection).getJarFile();
                classPathValue = decompress(jarFile.getName(), classPathDir);
                properties.setProperty(CLASS_PATH_VALUE, classPathValue);
                writeProperties(classPathDir, properties);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return classPathValue;
    }


    private static boolean isJarFile(URLConnection connection) {
        return connection instanceof JarURLConnection;
    }

    private static URLConnection getURLConnection() {
        URL url = ClassPathScanUtil.class.getResource("/");
        URLConnection connection = null;
        try {
            connection = url.openConnection();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return connection;
    }

    private static String getClassPathDir() {
        String configFilePath = getConfigFilePath();
        File file = new File(configFilePath);
        file.getParent();
        file = new File(file.getParent(), "classpath");
        if (file.exists()) {
            return file.getAbsolutePath();
        }
        file.mkdir();
        return file.getAbsolutePath();
    }

    private static File getPropertiesFile(String parent) {
        File file = new File(parent, PROPERTIES_FILE_NAMES);
        if (!file.exists()) {
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return file;
    }

    private static Properties getProperties(String parent) {
        Properties properties = new Properties();
        try {
            File file = getPropertiesFile(parent);
            InputStream is = new FileInputStream(file);
            properties.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return properties;
    }

    /**
     * 写入Properties信息
     **/
    public static void writeProperties(String parent, Properties properties) throws IOException {
        File file = getPropertiesFile(parent);
        OutputStream out = new FileOutputStream(file);
        properties.store(out, "");
        if(out != null){
            out.flush();
            out.close();
        }
    }

    /**
     * 解压JAR包
     *
     * @param jarFileName jar文件
     * @param outputPath  解压输出路径
     * @throws Exception IO异常
     */
    private static String decompress(String jarFileName, String outputPath) throws Exception {
        log.info("==========解压JAR包============：" + jarFileName);
        if (!outputPath.endsWith(File.separator)) {
            outputPath += File.separator;
        }
        JarFile jarFile = getJarFile(jarFileName);
        log.info("==========JAR包名称============：" + jarFile.getName());
        StringBuffer sb = new StringBuffer();
        for (Enumeration e = jarFile.entries(); e.hasMoreElements(); ) {
            JarEntry je = (JarEntry) e.nextElement();
            if (!(je.getName().startsWith(BOOT_INF))) continue;
            String outFileName = outputPath + je.getName();
            File f = new File(outFileName);
            if (je.getName().equals(BOOT_INF_CLASSES) || je.getName().startsWith(BOOT_INF_LIB)) {
                if (sb.length() > 0) sb.append(File.pathSeparator);
                sb.append(f.getAbsolutePath());
            }
            /**创建该路径的目录和所有父目录**/
            makeSupDir(outFileName);
            /**如果是目录,则直接进入下一个循环**/
            if (f.isDirectory()) {
                continue;
            }
            /**写文件**/
            writeFile(jarFile, je, f);
        }
        return sb.toString();
    }

    private static JarFile getJarFile(String jarFileName) throws Exception {
        if (!jarFileName.startsWith("jar:file:")) {
            jarFileName = "jar:file:" + File.separator + jarFileName;
        }
        URL url = new URL(jarFileName);
        JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
        return jarURLConnection.getJarFile();
    }

    private static void writeFile(JarFile jarFile, JarEntry je, File f) throws IOException {
        InputStream in = null;
        OutputStream out = null;
        try {
            in = jarFile.getInputStream(je);
            out = new BufferedOutputStream(new FileOutputStream(f));
            byte[] buffer = new byte[2048];
            int nBytes = 0;
            while ((nBytes = in.read(buffer)) > 0) {
                out.write(buffer, 0, nBytes);
            }
        } catch (IOException ioe) {
            throw ioe;
        } finally {
            try {
                if (null != out) {
                    out.flush();
                    out.close();
                }
            } catch (IOException ioe) {
                throw ioe;
            } finally {
                if (null != in) {
                    in.close();
                }
            }
        }
    }

    /**
     * 循环创建父目录
     *
     * @param outFileName
     */
    private static void makeSupDir(String outFileName) {
        //匹配分隔符
        Pattern p = Pattern.compile("[////]");
        Matcher m = p.matcher(outFileName);
        //每找到一个匹配的分隔符,则创建一个该分隔符以前的目录
        while (m.find()) {
            int index = m.start();
            String subDir = outFileName.substring(0, index);
            File subDirFile = new File(subDir);
            if (!subDirFile.exists())
                subDirFile.mkdir();
        }
    }

}